/*
 * Copyright (C) 2006, 2007, 2008 Apple Inc.  All rights reserved.
 * Copyright (C) 2007 Matt Lilek (pewtermoose@gmail.com).
 * Copyright (C) 2009 Joseph Pecoraro
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 *
 * 1.  Redistributions of source code must retain the above copyright
 *     notice, this list of conditions and the following disclaimer.
 * 2.  Redistributions in binary form must reproduce the above copyright
 *     notice, this list of conditions and the following disclaimer in the
 *     documentation and/or other materials provided with the distribution.
 * 3.  Neither the name of Apple Computer, Inc. ("Apple") nor the names of
 *     its contributors may be used to endorse or promote products derived
 *     from this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY APPLE AND ITS CONTRIBUTORS "AS IS" AND ANY
 * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
 * DISCLAIMED. IN NO EVENT SHALL APPLE OR ITS CONTRIBUTORS BE LIABLE FOR ANY
 * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
 * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */

/*
 * Modified from WebInspector (the developer tool of WebKit)
 * Do JavaScript syntax highlight
 */

var SourceSyntaxHighlighter = function(element, linePicker, cellPicker, finishCallback)
{
    this.sourceList = element;
    this.linePicker = "function" === typeof linePicker ? linePicker : function(lst) { return (lst && lst.childNodes) || [] };
    this.cellPicker = "function" === typeof cellPicker ? cellPicker : function(row) { return row };
    this.finishCallback = "function" === typeof finishCallback ? finishCallback : function() {};
}

SourceSyntaxHighlighter.prototype = {
    bind: function(func, thisObject)
    {
        var args = Array.prototype.slice.call(arguments, 2);
        return function() { return func.apply(thisObject, args.concat(Array.prototype.slice.call(arguments, 0))) };
    },

    removeChildren: function(node)
    {
        node.innerHTML = "";
    },

    createSpan: function(content, className)
    {
        var span = document.createElement("span");
        span.className = className;
        span.appendChild(document.createTextNode(content));
        return span;
    },

    // Do syntax hightlight line by line
    process: function()
    {
        // Split up the work into chunks so we don't block the
        // UI thread while processing.

        var rows = this.linePicker(this.sourceList);
        var rowsLength = rows.length;
        var tokensPerChunk = 100;
        var lineLengthLimit = 20000;

        var boundProcessChunk = this.bind(processChunk, this);
        var processChunkInterval = setInterval(boundProcessChunk, 25);
        boundProcessChunk();

        function processChunk()
        {
            for (var i = 0; i < tokensPerChunk; i++) {
                if (this.cursor >= this.lineCode.length)
                    moveToNextLine.call(this);
                if (this.lineIndex >= rowsLength) {
                    this.finishCallback.apply(this);
                    return;
                }
                if (this.cursor > lineLengthLimit) {
                    var codeFragment = this.lineCode.substring(this.cursor);
                    this.nonToken += codeFragment;
                    this.cursor += codeFragment.length;
                }

                this.lex();
            }
        }

        function moveToNextLine()
        {
            this.appendNonToken();

            var row = this.lineIndex < 0 ? undefined : rows[this.lineIndex];
            var line = row ? this.cellPicker(row) : null;
            if (line && this.newLine) {
                this.removeChildren(line);
                line.parentNode.insertBefore(this.newLine, line);
                line.parentNode.removeChild(line);

                this.newLine = null;
            }
            this.lineIndex++;
            if (this.lineIndex >= rowsLength && processChunkInterval) {
                clearInterval(processChunkInterval);
//                this.finishCallback.apply(this);
                return;
            }
            row = rows[this.lineIndex];
            line = row ? this.cellPicker(row) : null;

            this.lineCode = line.textContent;
            this.newLine = line.cloneNode(false);
            this.cursor = 0;
            if (!this.lineCode.length) moveToNextLine.call(this);
        }
    },

    lex: function()
    {
        var token = null;
        var codeFragment = this.lineCode.substring(this.cursor);
        var rulesLength = this.rules.length;

        for (var i = 0; i < rulesLength; i++) {
            var rule = this.rules[i];
            var ruleContinueStateCondition = typeof rule.continueStateCondition === "undefined" ? this.ContinueState.None : rule.continueStateCondition;
            if (this.continueState === ruleContinueStateCondition) {
                if (typeof rule.lexStateCondition !== "undefined" && this.lexState !== rule.lexStateCondition)
                    continue;
                var match = rule.pattern.exec(codeFragment);
                if (match) {
                    token = match[0];
                    if (token) {
                        if (!rule.dontAppendNonToken)
                            this.appendNonToken();
                        return rule.action.call(this, token);
                    }
                }
            }
        }
        this.nonToken += codeFragment.charAt(0);
        this.cursor++;
    },

    appendNonToken: function ()
    {
        if (this.nonToken.length > 0) {
            this.newLine.appendChild(document.createTextNode(this.nonToken));
            this.nonToken = "";
        }
    },

    // Do syntax highlight in bulk
    syntaxHighlightNode: function(node)
    {
        this.lineCode = node.textContent;
        this.removeChildren(node);
        this.newLine = node;
        this.cursor = 0;
        while (true) {
            if (this.cursor >= this.lineCode.length) {
                var codeFragment = this.lineCode.substring(this.cursor);
                this.nonToken += codeFragment;
                this.cursor += codeFragment.length;
                this.appendNonToken();
                this.newLine = null;
                return;
            }

            this.lex();
        }
        this.finishCallback.apply(this);
    }
}

JavaScriptSourceSyntaxHighlighter = function(element, linePicker, cellPicker, finishCallback) {
    SourceSyntaxHighlighter.call(this, element, linePicker, cellPicker, finishCallback);

    this.LexState = {
        Initial: 1,
        DivisionAllowed: 2
    };
    this.ContinueState = {
        None: 0,
        Comment: 1,
        SingleQuoteString: 2,
        DoubleQuoteString: 3,
        RegExp: 4
    };

    this.nonToken = "";
    this.cursor = 0;
    this.lineIndex = -1;
    this.lineCode = "";
    this.newLine = null;
    this.lexState = this.LexState.Initial;
    this.continueState = this.ContinueState.None;

    this.rules = [{
        pattern: /^[\s\xA0]+/,
        action: whiteSpaceAction,
        dontAppendNonToken: true
    }, {
        pattern: /^(?:\/\/.*)/,
        action: singleLineCommentAction
    }, {
        pattern: /^(?:\/\*(?:[^\*]|\*[^\/])*\*+\/)/,
        action: multiLineSingleLineCommentAction
    }, {
        pattern: /^(?:\/\*(?:[^\*]|\*[^\/])*)/,
        action: multiLineCommentStartAction
    }, {
        pattern: /^(?:(?:[^\*]|\*[^\/])*\*+\/)/,
        action: multiLineCommentEndAction,
        continueStateCondition: this.ContinueState.Comment
    }, {
        pattern: /^.*/,
        action: multiLineCommentMiddleAction,
        continueStateCondition: this.ContinueState.Comment
    }, {
        pattern: /^(?:(?:0|[1-9]\d*)\.\d+?(?:[eE](?:\d+|\+\d+|-\d+))?|\.\d+(?:[eE](?:\d+|\+\d+|-\d+))?|(?:0|[1-9]\d*)(?:[eE](?:\d+|\+\d+|-\d+))?|0x[0-9a-fA-F]+|0X[0-9a-fA-F]+)/,
        action: numericLiteralAction
    }, {
        pattern: /^(?:"(?:[^"\\]|\\(?:['"\bfnrtv]|[^'"\bfnrtv0-9xu]|0|x[0-9a-fA-F][0-9a-fA-F]|(?:u[0-9a-fA-F][0-9a-fA-F][0-9a-fA-F][0-9a-fA-F])))*"|'(?:[^'\\]|\\(?:['"\bfnrtv]|[^'"\bfnrtv0-9xu]|0|x[0-9a-fA-F][0-9a-fA-F]|(?:u[0-9a-fA-F][0-9a-fA-F][0-9a-fA-F][0-9a-fA-F])))*')/,
        action: stringLiteralAction
    }, {
        pattern: /^(?:'(?:[^'\\]|\\(?:['"\bfnrtv]|[^'"\bfnrtv0-9xu]|0|x[0-9a-fA-F][0-9a-fA-F]|(?:u[0-9a-fA-F][0-9a-fA-F][0-9a-fA-F][0-9a-fA-F])))*)\\$/,
        action: singleQuoteStringStartAction
    }, {
        pattern: /^(?:(?:[^'\\]|\\(?:['"\bfnrtv]|[^'"\bfnrtv0-9xu]|0|x[0-9a-fA-F][0-9a-fA-F]|(?:u[0-9a-fA-F][0-9a-fA-F][0-9a-fA-F][0-9a-fA-F])))*')/,
        action: singleQuoteStringEndAction,
        continueStateCondition: this.ContinueState.SingleQuoteString
    }, {
        pattern: /^(?:(?:[^'\\]|\\(?:['"\bfnrtv]|[^'"\bfnrtv0-9xu]|0|x[0-9a-fA-F][0-9a-fA-F]|(?:u[0-9a-fA-F][0-9a-fA-F][0-9a-fA-F][0-9a-fA-F])))*)\\$/,
        action: singleQuoteStringMiddleAction,
        continueStateCondition: this.ContinueState.SingleQuoteString
    }, {
        pattern: /^(?:"(?:[^"\\]|\\(?:['"\bfnrtv]|[^'"\bfnrtv0-9xu]|0|x[0-9a-fA-F][0-9a-fA-F]|(?:u[0-9a-fA-F][0-9a-fA-F][0-9a-fA-F][0-9a-fA-F])))*)\\$/,
        action: doubleQuoteStringStartAction
    }, {
        pattern: /^(?:(?:[^"\\]|\\(?:['"\bfnrtv]|[^'"\bfnrtv0-9xu]|0|x[0-9a-fA-F][0-9a-fA-F]|(?:u[0-9a-fA-F][0-9a-fA-F][0-9a-fA-F][0-9a-fA-F])))*")/,
        action: doubleQuoteStringEndAction,
        continueStateCondition: this.ContinueState.DoubleQuoteString
    }, {
        pattern: /^(?:(?:[^"\\]|\\(?:['"\bfnrtv]|[^'"\bfnrtv0-9xu]|0|x[0-9a-fA-F][0-9a-fA-F]|(?:u[0-9a-fA-F][0-9a-fA-F][0-9a-fA-F][0-9a-fA-F])))*)\\$/,
        action: doubleQuoteStringMiddleAction,
        continueStateCondition: this.ContinueState.DoubleQuoteString
    }, {
        pattern: /^(?:(?:[a-zA-Z]|[$_]|\\(?:u[0-9a-fA-F][0-9a-fA-F][0-9a-fA-F][0-9a-fA-F]))(?:(?:[a-zA-Z]|[$_]|\\(?:u[0-9a-fA-F][0-9a-fA-F][0-9a-fA-F][0-9a-fA-F]))|[0-9])*)/,
        action: identOrKeywordAction
    }, {
        pattern: /^\)/,
        action: rightParenAction,
        dontAppendNonToken: true
    }, {
        pattern: /^(?:<=|>=|===|==|!=|!==|\+\+|\-\-|<<|>>|>>>|&&|\|\||\+=|\-=|\*=|%=|<<=|>>=|>>>=|&=|\|=|^=|[{}\(\[\]\.;,<>\+\-\*%&\|\^!~\?:=])/,
        action: punctuatorAction,
        dontAppendNonToken: true
    }, {
        pattern: /^(?:\/=?)/,
        action: divPunctuatorAction,
        lexStateCondition: this.LexState.DivisionAllowed,
        dontAppendNonToken: true
    }, {
        pattern: /^(?:\/(?:(?:\\.)|[^\\*\/])(?:(?:\\.)|[^\\/])*\/(?:(?:[a-zA-Z]|[$_]|\\(?:u[0-9a-fA-F][0-9a-fA-F][0-9a-fA-F][0-9a-fA-F]))|[0-9])*)/,
        action: regExpLiteralAction
    }, {
        pattern: /^(?:\/(?:(?:\\.)|[^\\*\/])(?:(?:\\.)|[^\\/])*)\\$/,
        action: regExpStartAction
    }, {
        pattern: /^(?:(?:(?:\\.)|[^\\/])*\/(?:(?:[a-zA-Z]|[$_]|\\(?:u[0-9a-fA-F][0-9a-fA-F][0-9a-fA-F][0-9a-fA-F]))|[0-9])*)/,
        action: regExpEndAction,
        continueStateCondition: this.ContinueState.RegExp
    }, {
        pattern: /^(?:(?:(?:\\.)|[^\\/])*)\\$/,
        action: regExpMiddleAction,
        continueStateCondition: this.ContinueState.RegExp
    }];

    function whiteSpaceAction(token)
    {
        this.cursor += token.length;
        this.nonToken += token;
        this.lexState = this.LexState.Initial;
    }

    function singleLineCommentAction(token)
    {
        this.cursor += token.length;
        this.newLine.appendChild(this.createSpan(token, "js-comment"));
    }

    function multiLineSingleLineCommentAction(token)
    {
        this.cursor += token.length;
        this.newLine.appendChild(this.createSpan(token, "js-comment"));
    }

    function multiLineCommentStartAction(token)
    {
        this.cursor += token.length;
        this.newLine.appendChild(this.createSpan(token, "js-comment"));
        this.continueState = this.ContinueState.Comment;
    }

    function multiLineCommentEndAction(token)
    {
        this.cursor += token.length;
        this.newLine.appendChild(this.createSpan(token, "js-comment"));
        this.continueState = this.ContinueState.None;
    }

    function multiLineCommentMiddleAction(token)
    {
        this.cursor += token.length;
        this.newLine.appendChild(this.createSpan(token, "js-comment"));
    }

    function numericLiteralAction(token)
    {
        this.cursor += token.length;
        this.newLine.appendChild(this.createSpan(token, "js-number"));
        this.lexState = this.LexState.DivisionAllowed;
    }

    function stringLiteralAction(token)
    {
        this.cursor += token.length;
        this.newLine.appendChild(this.createSpan(token, "js-string"));
        this.lexState = this.LexState.Initial;
    }

    function singleQuoteStringStartAction(token)
    {
        this.cursor += token.length;
        this.newLine.appendChild(this.createSpan(token, "js-string"));
        this.continueState = this.ContinueState.SingleQuoteString;
    }

    function singleQuoteStringEndAction(token)
    {
        this.cursor += token.length;
        this.newLine.appendChild(this.createSpan(token, "js-string"));
        this.continueState = this.ContinueState.None;
    }

    function singleQuoteStringMiddleAction(token)
    {
        this.cursor += token.length;
        this.newLine.appendChild(this.createSpan(token, "js-string"));
    }

    function doubleQuoteStringStartAction(token)
    {
        this.cursor += token.length;
        this.newLine.appendChild(this.createSpan(token, "js-string"));
        this.continueState = this.ContinueState.DoubleQuoteString;
    }

    function doubleQuoteStringEndAction(token)
    {
        this.cursor += token.length;
        this.newLine.appendChild(this.createSpan(token, "js-string"));
        this.continueState = this.ContinueState.None;
    }

    function doubleQuoteStringMiddleAction(token)
    {
        this.cursor += token.length;
        this.newLine.appendChild(this.createSpan(token, "js-string"));
    }

    function regExpLiteralAction(token)
    {
        this.cursor += token.length;
        this.newLine.appendChild(this.createSpan(token, "js-regexp"));
        this.lexState = this.LexState.Initial;
    }

    function regExpStartAction(token)
    {
        this.cursor += token.length;
        this.newLine.appendChild(this.createSpan(token, "js-regexp"));
        this.continueState = this.ContinueState.RegExp;
    }

    function regExpEndAction(token)
    {
        this.cursor += token.length;
        this.newLine.appendChild(this.createSpan(token, "js-regexp"));
        this.continueState = this.ContinueState.None;
    }

    function regExpMiddleAction(token)
    {
        this.cursor += token.length;
        this.newLine.appendChild(this.createSpan(token, "js-regexp"));
    }

    var keywords = {
        "null": true,
        "true": true,
        "false": true,
        "break": true,
        "case": true,
        "catch": true,
        "const": true,
        "default": true,
        "finally": true,
        "for": true,
        "instanceof": true,
        "new": true,
        "var": true,
        "continue": true,
        "function": true,
        "return": true,
        "void": true,
        "delete": true,
        "if": true,
        "this": true,
        "do": true,
        "while": true,
        "else": true,
        "in": true,
        "switch": true,
        "throw": true,
        "try": true,
        "typeof": true,
        "debugger": true,
        "class": true,
        "enum": true,
        "export": true,
        "extends": true,
        "import": true,
        "super": true,
        "get": true,
        "set": true
    };

    var classes = {
        "Array": true,
        "Attr": true,
        "Boolean": true,
        "Comment": true,
        "DOMException": true,
        "DOMImplementation": true,
        "Date": true,
        "Document": true,
        "DocumentFragment": true,
        "Element": true,
        "Error": true,
        "Event": true,
        "Function": true,
        "Image": true,
        "Math": true,
        "Node": true,
        "Number": true,
        "Object": true,
        "Option": true,
        "RegExp": true,
        "String": true,
        "Text": true
    };

    var functions = {
        // Globals
        "decodeURI": true,
        "decodeURIComponent": true,
        "encodeURI": true,
        "encodeURIComponent": true,
        "escape": true,
        "eval": true,
        "isFinite": true,
        "isNaN": true,
        "parseFloat": true,
        "parseInt": true,
        "unescape": true,
        // window.*
        "alert": true,
        "clearInterval": true,
        "clearTimeout": true,
        "confirm": true,
        "getComputedStyle": true,
        "prompt": true,
        "setInterval": true,
        "setTimeout": true,
        // document.*
        "createAttribute": true,
        "createComment": true,
        "createDocumentFragment": true,
        "createElement": true,
        "createEvent": true,
        "createTextNode": true,
        "getElementById": true,
        "getElementsByName": true,
        "getElementsByTagName": true,
        "importNode": true,
        // Object.*
        "toString": true,
        // Element.*
        "addEventListener": true,
        "removeEventListener": true
    };

    var variables = {
        // Globals
        "Infinity": true,
        "NaN": true,
        "undefined": true,
        "arguments": true,
        // window.*
        "defaultStatus": true,
        "document": true,
        "history": true,
        "location": true,
        "navigator": true,
        "opener": true,
        "parent": true,
        "screen": true,
        "self": true,
        "window": true,
        // Element.*
        "className": true,
        "innerHTML": true,
        "innerText": true,
        "outerHTML": true,
        "outerText": true,
        "parentNode": true,
        "tagName": true,
        "textContent": true
    };
        
    function identOrKeywordAction(token)
    {
        this.cursor += token.length;
        this.lexState = this.LexState.Initial;

        if (token in keywords) {
            this.newLine.appendChild(this.createSpan(token, "js-keyword"));
        } else if (token in classes) {
            this.newLine.appendChild(this.createSpan(token, "js-class"));
//        } else if (token in functions) {
//            this.newLine.appendChild(this.createSpan(token, "js-function"));
        } else if (token in variables) {
            this.newLine.appendChild(this.createSpan(token, "js-variable"));
        } else {
            this.newLine.appendChild(this.createSpan(token, "js-ident"));
            this.lexState = this.LexState.DivisionAllowed;
        }
    }

    function divPunctuatorAction(token)
    {
        this.cursor += token.length;
        this.nonToken += token;
        this.lexState = this.LexState.Initial;
    }

    function rightParenAction(token)
    {
        this.cursor += token.length;
        this.nonToken += token;
        this.lexState = this.LexState.DivisionAllowed;
    }

    function punctuatorAction(token)
    {
        this.cursor += token.length;
        this.nonToken += token;
        this.lexState = this.LexState.Initial;
    }
}

JavaScriptSourceSyntaxHighlighter.prototype.__proto__ = SourceSyntaxHighlighter.prototype;
